using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace Oops.Xna.Framework.Viewpoint
{
	/// <summary>
	/// A <see cref="Camera"/> callback to retrieve dynamic transform data.
	/// </summary>
	public interface ICameraCallback
	{
		/// <summary>
		/// Returns the transform of a given entity.
		/// </summary>
		/// <param name="position">A <see cref="Microsoft.Xna.Framework.Vector3"/> representing the position of an entity.</param>
		/// <param name="orientation">A <see cref="Microsoft.Xna.Framework.Matrix"/> representing the orientation of an entity.</param>
		void GetTransform(ref Vector3 position, ref Matrix orientation);
	}

	/// <summary>
	/// Specifies the horizontal alignment of a <see cref="Camera"/> instance's viewport.
	/// </summary>
	public enum ViewportHorizontalAlignment
	{
		/// <summary>
		/// No alignment.
		/// </summary>
		None,

		/// <summary>
		/// Aligns with the left side of the screen.
		/// </summary>
		Left,

		/// <summary>
		/// Aligns with the right side of the screen.
		/// </summary>
		Right,

		/// <summary>
		/// Aligns with the left and right side of the screen.
		/// </summary>
		Full
	}

	/// <summary>
	/// Specifies the vertical alignment of a <see cref="Camera"/> instance's viewport.
	/// </summary>
	public enum ViewportVerticalAlignment
	{
		/// <summary>
		/// No alignment.
		/// </summary>
		None,

		/// <summary>
		/// Aligns with the top of the screen.
		/// </summary>
		Top,

		/// <summary>
		/// Aligns with the bottom of the screen.
		/// </summary>
		Bottom,

		/// <summary>
		/// Aligns with the top and bottom of the screen.
		/// </summary>
		Full
	}

	/// <summary>
	/// Represents a viewpoint in 3D space.
	/// </summary>
	public class Camera : GameComponent
	{
		private IViewpointService viewpointService;

		private string name;

		public Vector3 Position;
		public Matrix Orientation = Matrix.Identity;

		/// <summary>
		/// Gets or sets the view matrix. Use this matrix to change the position and direction of the <see cref="Camera"/>.
		/// </summary>
		public Matrix View = Matrix.Identity;

		/// <summary>
		/// Gets the projection matrix. This matrix is used to convert a 3D image to a 2D image that is rendered to the computer screen.
		/// </summary>
		/// <remarks>
		/// Do not set this field directly.  If a projection other than perspective field of view is 
		/// necessary, override the <see cref="Camera.UpdateProjection"/> method.
		/// </remarks>
		public Matrix Projection = Matrix.Identity;

		/// <summary>
		/// Gets or sets the window dimensions of a render-target surface onto which a 3D volume projects.
		/// </summary>
		public Viewport Viewport;

		private float fieldOfView = MathHelper.PiOver4;
		private float nearPlaneDistance = 0.1f;
		private float farPlaneDistance = 1000.0f;

		private ViewportHorizontalAlignment horizontalAlignment = ViewportHorizontalAlignment.Full;
		private ViewportVerticalAlignment verticalAlignment = ViewportVerticalAlignment.Full;

		private bool updateFrustum = true;
		private BoundingFrustum frustum = new BoundingFrustum(Matrix.Identity);

		/// <summary>
		/// Creates an instance of the <see cref="Camera"/> class.
		/// </summary>
		/// <param name="game">The associated <see cref="Microsoft.Xna.Framework.Game"/>.</param>
		public Camera(Game game, string name) : base(game)
		{
			// The game parameter must exist.
			if (game == null)
			{
				throw new ArgumentNullException("game");
			}

			// The name parameter must be specified.
			if (String.IsNullOrEmpty(name))
			{
				throw new ArgumentNullException("name");
			}

			// Hold onto parameters.
			this.name = name;
		}

		/// <summary>
		/// Gets the name of the <see cref="Camera"/>.
		/// </summary>
		public string Name
		{
			get { return this.name; }
		}

		/// <summary>
		/// Gets or sets the field of view of the camera.
		/// </summary>
		/// <remarks>
		/// The field of view should be defined in radians.  The default value is 1000.
		/// </remarks>
		public float FieldOfView
		{
			get { return this.fieldOfView; }
			set
			{
				if (this.fieldOfView != value)
				{
					this.fieldOfView = value;
					this.UpdateProjection();
				}
			}
		}

		/// <summary>
		/// Gets or sets the near plane of the <see cref="Camera"/> instance's projection.
		/// </summary>
		public float NearPlaneDistance
		{
			get { return this.nearPlaneDistance; }
			set
			{
				if (this.nearPlaneDistance != value)
				{
					this.nearPlaneDistance = value;
					this.UpdateProjection();
				}
			}
		}

		/// <summary>
		/// Gets or sets the far plane of the <see cref="Camera"/> instance's projection.
		/// </summary>
		public float FarPlaneDistance
		{
			get { return this.farPlaneDistance; }
			set
			{
				if (this.farPlaneDistance != value)
				{
					this.farPlaneDistance = value;
					this.UpdateProjection();
				}
			}
		}

		/// <summary>
		/// Gets or sets horizontal alignment of the <see cref="Camera.viewport" />.
		/// </summary>
		public ViewportHorizontalAlignment HorizontalAlignment
		{
			get { return this.horizontalAlignment; }
			set
			{
				if (this.horizontalAlignment != value)
				{
					this.horizontalAlignment = value;
					this.UpdateViewport();
				}
			}
		}

		/// <summary>
		/// Gets or sets vertical alignment of the <see cref="Camera.viewport" />.
		/// </summary>
		public ViewportVerticalAlignment VerticalAlignment
		{
			get { return this.verticalAlignment; }
			set
			{
				if (this.verticalAlignment != value)
				{
					this.verticalAlignment = value;
					this.UpdateViewport();
				}
			}
		}

		protected IViewpointService ViewpointService
		{
			get { return this.viewpointService; }
		}

		public void Project(Vector2 source, out Vector3 position, out Vector3 vector)
		{
			position = this.Viewport.Unproject(
				new Vector3(source, 0.0f),
				this.Projection,
				this.View,
				Matrix.Identity);

			Vector3 far = this.Viewport.Unproject(
				new Vector3(source, 1.0f),
				this.Projection,
				this.View,
				Matrix.Identity);
#if INLINE
#if XBOX
			vector = new Vector3();
#endif
			vector.X = far.X - position.X;
			vector.Y = far.Y - position.Y;
			vector.Z = far.Z - position.Z;
#elif REFERENCE
			Vector3.Subtract(ref far, ref position, out vector);
#else
			vector = far - position;
#endif
		}

		/// <summary>
		/// Gets the frustrum of the <see cref="Camera"/>.
		/// </summary>
		/// <returns>
		/// Returns a <see cref="Microsoft.Xna.Framework.BoundingFrustum"/> to help determine 
		/// what forms can be seen from the <see cref="Camera"/>.
		/// </returns>
		public BoundingFrustum GetFrustum()
		{
			// Only calculate the frustum once per update/request.
			if (this.updateFrustum)
			{
#if INLINE
#if XBOX
				Matrix temp = MatrixExtension.Zero;
#else
				Matrix temp;
#endif
				temp.M11 = (this.View.M11 * this.Projection.M11) + (this.View.M12 * this.Projection.M21) + (this.View.M13 * this.Projection.M31) + (this.View.M14 * this.Projection.M41);
				temp.M12 = (this.View.M11 * this.Projection.M12) + (this.View.M12 * this.Projection.M22) + (this.View.M13 * this.Projection.M32) + (this.View.M14 * this.Projection.M42);
				temp.M13 = (this.View.M11 * this.Projection.M13) + (this.View.M12 * this.Projection.M23) + (this.View.M13 * this.Projection.M33) + (this.View.M14 * this.Projection.M43);
				temp.M14 = (this.View.M11 * this.Projection.M14) + (this.View.M12 * this.Projection.M24) + (this.View.M13 * this.Projection.M34) + (this.View.M14 * this.Projection.M44);

				temp.M21 = (this.View.M21 * this.Projection.M11) + (this.View.M22 * this.Projection.M21) + (this.View.M23 * this.Projection.M31) + (this.View.M24 * this.Projection.M41);
				temp.M22 = (this.View.M21 * this.Projection.M12) + (this.View.M22 * this.Projection.M22) + (this.View.M23 * this.Projection.M32) + (this.View.M24 * this.Projection.M42);
				temp.M23 = (this.View.M21 * this.Projection.M13) + (this.View.M22 * this.Projection.M23) + (this.View.M23 * this.Projection.M33) + (this.View.M24 * this.Projection.M43);
				temp.M24 = (this.View.M21 * this.Projection.M14) + (this.View.M22 * this.Projection.M24) + (this.View.M23 * this.Projection.M34) + (this.View.M24 * this.Projection.M44);

				temp.M31 = (this.View.M31 * this.Projection.M11) + (this.View.M32 * this.Projection.M21) + (this.View.M33 * this.Projection.M31) + (this.View.M34 * this.Projection.M41);
				temp.M32 = (this.View.M31 * this.Projection.M12) + (this.View.M32 * this.Projection.M22) + (this.View.M33 * this.Projection.M32) + (this.View.M34 * this.Projection.M42);
				temp.M33 = (this.View.M31 * this.Projection.M13) + (this.View.M32 * this.Projection.M23) + (this.View.M33 * this.Projection.M33) + (this.View.M34 * this.Projection.M43);
				temp.M34 = (this.View.M31 * this.Projection.M14) + (this.View.M32 * this.Projection.M24) + (this.View.M33 * this.Projection.M34) + (this.View.M34 * this.Projection.M44);

				temp.M41 = (this.View.M41 * this.Projection.M11) + (this.View.M42 * this.Projection.M21) + (this.View.M43 * this.Projection.M31) + (this.View.M44 * this.Projection.M41);
				temp.M42 = (this.View.M41 * this.Projection.M12) + (this.View.M42 * this.Projection.M22) + (this.View.M43 * this.Projection.M32) + (this.View.M44 * this.Projection.M42);
				temp.M43 = (this.View.M41 * this.Projection.M13) + (this.View.M42 * this.Projection.M23) + (this.View.M43 * this.Projection.M33) + (this.View.M44 * this.Projection.M43);
				temp.M44 = (this.View.M41 * this.Projection.M14) + (this.View.M42 * this.Projection.M24) + (this.View.M43 * this.Projection.M34) + (this.View.M44 * this.Projection.M44);

				this.frustum.Matrix = temp;
#elif REFERENCE
				Matrix temp;
				Matrix.Multiply(ref this.View, ref this.Projection, out temp);

				this.frustum.Matrix = temp;
#else
				this.frustum.Matrix = this.View * this.Projection;
#endif
				this.updateFrustum = false;
			}

			// Return the bounding frustum.
			return this.frustum;
		}

		#region GameComponent Members

		protected override void Dispose(bool disposing)
		{
			if (disposing)
			{
				// Unsubscribe from some graphic device events.
				// TODO : GC!
				this.Game.GraphicsDevice.DeviceReset -= new EventHandler(this.OnDeviceReset);
			}

			// Call inherited method.
			base.Dispose(disposing);
		}

		#endregion

		#region IGameComponent Members

		public override void Initialize()
		{
			// Get the viewpoint service.
			this.viewpointService = this.Game.GetService<IViewpointService>();
			
			// Subscribe to some graphic device events.
			// TODO : GC!
			this.Game.GraphicsDevice.DeviceReset += new EventHandler(this.OnDeviceReset);
			this.OnDeviceReset(this.Game.GraphicsDevice, EventArgs.Empty);

			// Call inherited method.
			base.Initialize();
		}

		#endregion

		private void OnDeviceReset(object sender, EventArgs e)
		{
			// Align the viewport with the back buffer.
			this.UpdateViewport();
			
			// Update the projection matrix.
			this.UpdateProjection();
		}

		#region IUpdateable Members

		public override void Update(GameTime gameTime)
		{
			// Reset update frustum flag.
			this.updateFrustum = true;

			// Update the view.
			this.UpdateView();

			// Call inherited method.
			base.Update(gameTime);
		}

		#endregion

		protected virtual void UpdateViewport()
		{
			// Default viewport clip volume.
			this.Viewport.MinDepth = this.Game.GraphicsDevice.Viewport.MinDepth;
			this.Viewport.MaxDepth = this.Game.GraphicsDevice.Viewport.MaxDepth;

			// Base horizontal viewport placement  and size on alignment.
			switch (this.horizontalAlignment)
			{
				case ViewportHorizontalAlignment.Left:
					this.Viewport.X = 0;
					this.Viewport.Width = this.Game.GraphicsDevice.PresentationParameters.BackBufferWidth >> 1;
					break;

				case ViewportHorizontalAlignment.Right:
					this.Viewport.X = this.Game.GraphicsDevice.PresentationParameters.BackBufferWidth >> 1;
					this.Viewport.Width = this.Game.GraphicsDevice.PresentationParameters.BackBufferWidth >> 1;
					break;

				case ViewportHorizontalAlignment.Full:
					this.Viewport.X = 0;
					this.Viewport.Width = this.Game.GraphicsDevice.PresentationParameters.BackBufferWidth;
					break;
			}

			// Base vertical viewport placement and size on alignment.
			switch (this.verticalAlignment)
			{
				case ViewportVerticalAlignment.Top:
					this.Viewport.Y = 0;
					this.Viewport.Height = this.Game.GraphicsDevice.PresentationParameters.BackBufferHeight >> 1;
					break;

				case ViewportVerticalAlignment.Bottom:
					this.Viewport.Y = this.Game.GraphicsDevice.PresentationParameters.BackBufferHeight >> 1;
					this.Viewport.Height = this.Game.GraphicsDevice.PresentationParameters.BackBufferHeight >> 1;
					break;

				case ViewportVerticalAlignment.Full:
					this.Viewport.Y = 0;
					this.Viewport.Height = this.Game.GraphicsDevice.PresentationParameters.BackBufferHeight;
					break;
			}
		}

		/// <summary>
		/// Updates the view matrix.
		/// </summary>
		protected virtual void UpdateView()
		{
			// Create the view matrix from the position and orientation.
#if REFERENCE
			MatrixExtension.CreateView(ref this.Position, ref this.Orientation, out this.View);
#else
			this.View = MatrixExtension.CreateView(this.Position, this.Orientation);
#endif
		}

		/// <summary>
		/// Updates the projection matrix.
		/// </summary>
		protected virtual void UpdateProjection()
		{
			// Use a perspective field of view for the projection matrix.
#if REFERENCE
			Matrix.CreatePerspectiveFieldOfView(
				this.fieldOfView,
				this.Viewport.AspectRatio,
				this.nearPlaneDistance,
				this.farPlaneDistance,
				out this.Projection);
#else
			this.Projection = Matrix.CreatePerspectiveFieldOfView(
				this.fieldOfView,
				this.Viewport.AspectRatio,
				this.nearPlaneDistance,
				this.farPlaneDistance);
#endif
		}
	}
}
